fix(factory): suppress concurrent duplicate dispatches#44
Conversation
📝 WalkthroughWalkthrough
ChangesConcurrent dispatch deduplication
Estimated code review effort🎯 2 (Simple) | ⏱️ ~5 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to suppress duplicate concurrent dispatches for the same issue by tracking in-flight dispatch promises in src/orchestrator/factory.ts. Feedback on the changes points out a potential issue where concurrent dispatches with different dryRun options could share the same in-flight promise, potentially suppressing a real dispatch with a dry-run dispatch. It is recommended to incorporate the dryRun option into the cache key to prevent this behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async dispatch(decision: TriageDecision, opts: { dryRun?: boolean } = {}): Promise<DispatchResult> { | ||
| const key = issueKey(decision.issue) | ||
| const inFlight = this.#dispatchInFlight.get(key) | ||
| if (inFlight) { | ||
| this.#increment('dispatchDuplicateSuppressed') | ||
| return inFlight | ||
| } | ||
|
|
||
| const dispatched = this.#dispatchUnlocked(decision, opts) | ||
| this.#dispatchInFlight.set(key, dispatched) | ||
| try { | ||
| return await dispatched | ||
| } finally { | ||
| if (this.#dispatchInFlight.get(key) === dispatched) { | ||
| this.#dispatchInFlight.delete(key) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
If dispatch is called concurrently with different dryRun options (for example, one real run and one dry run), they will share the same in-flight promise because the cache key only uses the issue key. This can lead to a real dispatch being suppressed by a dry-run dispatch, preventing agents from actually being spawned.
To resolve this, calculate the effective dryRun value and include it in the #dispatchInFlight map key.
| async dispatch(decision: TriageDecision, opts: { dryRun?: boolean } = {}): Promise<DispatchResult> { | |
| const key = issueKey(decision.issue) | |
| const inFlight = this.#dispatchInFlight.get(key) | |
| if (inFlight) { | |
| this.#increment('dispatchDuplicateSuppressed') | |
| return inFlight | |
| } | |
| const dispatched = this.#dispatchUnlocked(decision, opts) | |
| this.#dispatchInFlight.set(key, dispatched) | |
| try { | |
| return await dispatched | |
| } finally { | |
| if (this.#dispatchInFlight.get(key) === dispatched) { | |
| this.#dispatchInFlight.delete(key) | |
| } | |
| } | |
| } | |
| async dispatch(decision: TriageDecision, opts: { dryRun?: boolean } = {}): Promise<DispatchResult> { | |
| const dryRun = opts.dryRun ?? this.#config.dryRun | |
| const key = issueKey(decision.issue) + ':' + dryRun | |
| const inFlight = this.#dispatchInFlight.get(key) | |
| if (inFlight) { | |
| this.#increment('dispatchDuplicateSuppressed') | |
| return inFlight | |
| } | |
| const dispatched = this.#dispatchUnlocked(decision, opts) | |
| this.#dispatchInFlight.set(key, dispatched) | |
| try { | |
| return await dispatched | |
| } finally { | |
| if (this.#dispatchInFlight.get(key) === dispatched) { | |
| this.#dispatchInFlight.delete(key) | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04d1441a6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| async dispatch(decision: TriageDecision, opts: { dryRun?: boolean } = {}): Promise<DispatchResult> { | ||
| const key = issueKey(decision.issue) |
There was a problem hiding this comment.
Use Linear issue key for the dispatch lock
This lock is meant to suppress duplicate dispatches for the same Linear issue, but issueKey(decision.issue) includes the UUID and mount path (key:uuid:path), while the persisted dispatch guard is keyed only by issue.key. When concurrent detections for the same Linear key arrive through different issue paths/UUIDs (a scenario already covered by the repo's duplicate-path test case), both calls miss this in-process map and can still race past #dispatchBlockReason before either records inFlight, leaving the duplicate-spawn failure mode this change is supposed to prevent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/orchestrator/factory.ts`:
- Around line 1292-1293: The in-flight dispatch lock in the factory is using a
path/uuid-sensitive key via issueKey(decision.issue), which can differ from the
stable issue identity used by the dispatch-attempt blocking logic. Update the
in-flight lookup in the dispatch flow to use the same stable per-issue key as
the attempt blocker (the issue.key identity on decision.issue) so concurrent
events for the same Linear issue cannot bypass dedupe and race into duplicate
spawn handling. Keep the change localized around the code that reads
this.#dispatchInFlight and compares against the existing attempt-blocking logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 20fc9f29-03d7-48dc-b7e7-8c69ad61dedc
📒 Files selected for processing (1)
src/orchestrator/factory.ts
04d1441 to
325d2c5
Compare
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Review: PR #44 —
|
325d2c5 to
c010b20
Compare
|
Addressed PR feedback and CI failure. Changes:
Validation:
GitHub CI package check is now passing on commit c010b20. |
Review of PR #44 —
|
Summary
Why
During the real factory E2E run, duplicate live Linear events for the same issue could overlap while implementer/reviewer spawn was still in progress. The second dispatch attempted to spawn an already-created agent, hit
agent already exists, abandonedinFlight, and prevented the PR babysitter handoff from retaining state.Verification
npm run buildnpx vitest run src/orchestrator/factory.test.ts -t "does not double-dispatch the same issue from concurrent live events"AR-337dispatchedar-337-impl-default+ar-337-review; GitHub PR docs(ar-337): add clean dispatch proof for 20260624T073141Z #43 opened; factory spawnedar-337-babysit; unclear LinearAR-339posted Slack writeback at/slack/channels/C0BAC2KSTGV/messages/factory-c0bac2kstgv-cp49rh.json.Summary by cubic
Prevents double-dispatch for the same Linear issue by sharing the in-flight dispatch across concurrent events, including canonical and alias paths. This avoids duplicate agent spawns and state loss.
#dispatchInFlightpromise lock keyed by issue key + mode (dry-run/live) + phase (dispatch/escalation).dispatchDuplicateSuppressed.Written for commit c010b20. Summary will update on new commits.